# -*- coding: utf-8 -*-
import requests
import json
import posixpath
import paramiko
import subprocess
import socket

from django.conf import settings
from urllib.parse import quote, urlencode
from requests import exceptions
from django.utils.encoding import iri_to_uri, smart_str
from paramiko.ssh_exception import (
    SSHException, BadHostKeyException, NoValidConnectionsError, AuthenticationException
)


SPIDER_BASE_URL = 'http://' + settings.SPIDERSERVER + ':' \
                  + str(settings.SPIDERSERVERPORT) + '/'      # spider_server的url地址
SPIDER_RESTFUL_CONF = {
    'daemonstatus': {
        'method': 'get',
        'path': 'daemonstatus.json'
    },
    'addversion': {
        'method': 'post',
        'path': 'addversion.json'
    },
    'listprojects': {
        'method': 'get',
        'path': 'listprojects.json'
    },
    'listversions': {
        'method': 'get',
        'path': 'listversions.json'
    },
    'listspiders': {
        'method': 'get',
        'path': 'listspiders.json'
    },
    'listjobs': {
        'method': 'get',
        'path': 'listjobs.json'
    },
    'delversion': {
        'method': 'post',
        'path': 'delversion.json'
    },
    'delproject': {
        'method': 'post',
        'path': 'delproject.json'
    },
    'schedule': {
        'method': 'post',
        'path': 'schedule.json'
    },
    'cancel': {
        'method': 'post',
        'path': 'cancel.json'
    }
}

SSH_SERVER_HOST = settings.HOST
SSH_SERVER_PORT = settings.PORT
SSH_SERVER_USER = settings.USER
SSH_SERVER_PASSWORD = settings.PASSWORD


def exec_command_with_all_result(cmd, is_remote=True):
    """
    执行命令
    :param cmd: 命令
    :param is_remote: 是否远程执行

    :raises BadHostKeyException: if the server's host key could not be verified
    :raises AuthenticationException: if authentication failed
    :raises SSHException: if there was any other error connecting or establishing an SSH session
    :raises socket.error: if a socket error occurred while connecting

    :return: out输出和error输出
    """
    if is_remote:
        try:
            ssh = paramiko.SSHClient()
            ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
            ssh.connect(SSH_SERVER_HOST, SSH_SERVER_PORT,
                        SSH_SERVER_USER, SSH_SERVER_PASSWORD, timeout=10)
            stdin, stdout, stderr = ssh.exec_command(cmd)
            # stdin.write("Y")   #简单交互，输入 ‘Y’
            out = stdout.read()
            err = stderr.read()

            # LOG.info(out)
            # LOG.info('%s\tOK\n' % (HIVE_HOST))
            # LOG.error(err)

            channel = stdout.channel
            status = channel.recv_exit_status()
            # LOG.debug(status)
            ssh.close()

            return out, err, status
        except BadHostKeyException as badHostKeyException:
            raise badHostKeyException
        except AuthenticationException as authenticationException:
            raise authenticationException
        except SSHException as sshException:
            raise sshException
        except NoValidConnectionsError as noValidConnectionsError:
            raise noValidConnectionsError
        except socket.error as socketError:
            raise socketError
    else:
        p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        out = ''
        err = ''
        if p.stdout:
            out = p.stdout.read()
            # print out
            # LOG.info(out)
        # if p.stderr:
        #     err = p.stderr.read()
        #     LOG.error(err)

        retval = p.wait()
        # LOG.info('\n=============================\nretval = %d' % (retval))
        return out, err, retval


class RestException(Exception):
    """
    Any error result from the Rest API is converted into this exception type.
    """

    def __init__(self, error):
        Exception.__init__(self, error)
        self._error = error
        self._code = None
        self._message = str(error)
        self._headers = {}

        # Get more information if urllib2.HTTPError.
        try:
            self._code = error.response.status_code
            self._headers = error.response.headers
            self._message = self._error.response.text
        except AttributeError:
            pass

    def __str__(self):
        res = self._message or ""
        if self._code is not None:
            res += " (error %s)" % self._code
        return res

    def get_parent_ex(self):
        if isinstance(self._error, Exception):
            return self._error
        return None

    @property
    def code(self):
        return self._code

    @property
    def message(self):
        return self._message


class SpiderClient(object):
    """
    Basic HTTP client tailored for rest APIs.
    """

    def __init__(self, base_url):
        """
        @param base_url: The base url to the API.
        """
        self._base_url = base_url.rstrip('/')
        self._session = requests.Session()
        self._exc_class = RestException

    @property
    def base_url(self):
        return self._base_url

    def execute(self, http_method, path, params=None):
        """
        Submit an HTTP request.
        @param http_method: GET, POST, PUT, DELETE
        @param path: The path of the resource. Unsafe characters will be quoted.
        @param params: Key-value parameter data.

        @return: The result of urllib2.urlopen()
        """
        # Prepare URL
        path = quote(smart_str(path))
        url = self._make_url(path, params)

        # Get Data from spider server
        try:
            resp = getattr(self._session, http_method.lower())(url)
            if resp.status_code >= 300:
                resp.raise_for_status()
                raise exceptions.HTTPError(response=resp)
            return resp
        except exceptions.ConnectionError as connectionError:
            raise self._exc_class(connectionError)
        except exceptions.HTTPError as hTTPError:
            raise self._exc_class(hTTPError)
        except exceptions.RequestException as requestException:
            raise self._exc_class(requestException)

    def _make_url(self, path, params):
        """
        拼接url路径及参数
        :param path: 服务器端基本url后的路径
        :param params: 传递的参数
        :return:
        """
        res = self._base_url
        if path:
            res += posixpath.normpath('/' + path.lstrip('/'))
        if params:
            param_str = urlencode(params)
            res += '?' + param_str
        return iri_to_uri(res)


class SpiderManager(object):
    """
    爬虫管理
    """
    def __init__(self, base_url=SPIDER_BASE_URL):
        self._client = SpiderClient(base_url=base_url)

    @property
    def client(self):
        return self._client

    def get_daemon_status(self):
        """
        To check the load status of a service.
        :return: Json response dic
        """

        # response example:
        # {"node_name": "impala01", "status": "ok", "pending": 0, "running": 0, "finished": 2}
        r = self._client.execute(SPIDER_RESTFUL_CONF['daemonstatus']['method'],
                                 SPIDER_RESTFUL_CONF['daemonstatus']['path'])
        return json.loads(bytes.decode(r.content))

    def add_version(self, project, version, egg):
        """
        Add a version to a project, creating the project if it doesn’t exist.
        :param project: (string, required) - the project name
        :param version: (string, required) - the project version
        :param egg: (file, required) - a Python egg containing the project’s code
        :return: Json response dic
        """
        parameters = {
            'project': project,
            'version': version,
            'egg': egg
        }

        r = self._client.execute(SPIDER_RESTFUL_CONF['addversion']['method'],
                                 SPIDER_RESTFUL_CONF['addversion']['path'],
                                 parameters)
        return json.loads(bytes.decode(r.content))

    def schedule(self, project, spider, setting=None, job_id=None, version=None, **kwargs):
        """
        Schedule a spider run (also known as a job), returning the job id.
        :param project: (string, required) - the project name
        :param spider: (string, required) - the spider name
        :param setting: (string, optional) - a Scrapy setting to use when running the spider
        :param job_id: (string, optional) - a job id used to identify the job, overrides the default generated UUID
        :param version: (string, optional) - the version of the project to use
        :return: Json response dic
        """
        data = {
            'project': project,
            'spider': spider
        }

        cmd = 'curl %sschedule.json -d project=%s -d spider=%s' % (SPIDER_BASE_URL, project, spider)

        if setting:
            data.setdefault('setting', setting)
            for item in setting:
                cmd = cmd + ' -d setting=' + item
        if job_id:
            data.setdefault('jobid', job_id)
            cmd = cmd + ' -d jobid=' + job_id
        if version:
            data.setdefault('_version', version)
            cmd = cmd + ' -d _version=' + version
        # data = dict(data, **kwargs)

        for key, value in kwargs.items():
            cmd = cmd + ' -d {0}={1}'.format(key, value)

        # r = self._client.execute(SPIDER_RESTFUL_CONF['schedule']['method'],
        #                          SPIDER_RESTFUL_CONF['schedule']['path'],
        #                          data=data)

        out, err, status = exec_command_with_all_result(cmd)
        return out
        # return json.loads(bytes.decode(r.content))

    def cancel(self, project, job_id):
        """
        Cancel a spider run (aka. job). If the job is pending, it will be removed. If the job is running,
        it will be terminated.
        :param project: (string, required) - the project name
        :param job_id: (string, required) - the job id
        :return: Json response dic
        """
        parameters = {
            'project': project,
            'job': job_id
        }

        r = self._client.execute(SPIDER_RESTFUL_CONF['cancel']['method'],
                                 SPIDER_RESTFUL_CONF['cancel']['path'],
                                 parameters)
        return json.loads(bytes.decode(r.content))

    def list_projects(self):
        """
        Get the list of projects uploaded to this Scrapy server.
        :return: Json response dic
        """

        # response example:
        # {"node_name": "impala01", "status": "ok", "projects": ["openCourse", "default"]}
        r = self._client.execute(SPIDER_RESTFUL_CONF['listprojects']['method'],
                                 SPIDER_RESTFUL_CONF['listprojects']['path'])
        return json.loads(bytes.decode(r.content))

    def list_versions(self, project):
        """
        Get the list of versions available for some project.
        The versions are returned in order, the last one is the currently used version.
        :param project: (string, required) - the project name
        :return:
        """

        # response example:
        # {"node_name": "impala01", "status": "ok", "versions": ["1497840158"]}
        parameters = {'project': project}
        r = self._client.execute(SPIDER_RESTFUL_CONF['listversions']['method'],
                                 SPIDER_RESTFUL_CONF['listversions']['path'],
                                 parameters)
        return json.loads(bytes.decode(r.content))

    def list_spiders(self, project, version=None):
        """
        Get the list of spiders available in the last (unless overridden) version of some project.
        :param project: (string, required) - the project name
        :param version: (string, optional) - the version of the project to examine
        :return: Json response dic
        """

        # response example:
        # {"node_name": "impala01", "status": "ok", "spiders": ["163", "AC", "BC", "CMU", "DUKE", "EDX", "Yale"]}
        parameters = {'project': project}
        if version:
            parameters.setdefault('_version', version)
        r = self._client.execute(SPIDER_RESTFUL_CONF['listspiders']['method'],
                                 SPIDER_RESTFUL_CONF['listspiders']['path'],
                                 parameters)
        return json.loads(bytes.decode(r.content))

    def list_jobs(self, project):
        """
        Get the list of pending, running and finished jobs of some project.
        :param project: (string, required) - the project name
        :return: Json response dic
        """

        # response example:
        # {"node_name": "impala01", "status": "ok", "pending": [], "running": [],
        # "finished": [{"id": "5f3c4670556a11e7a990fa163e243df5", "spider": "163",
        # "start_time": "2017-06-20 11:41:44.181485", "end_time": "2017-06-20 11:42:15.770733"},
        # {"id": "02b6a526556f11e7a990fa163e243df5", "spider": "163", "start_time": "2017-06-20 12:14:54.182140",
        # "end_time": "2017-06-20 12:14:56.279716"}]}
        parameters = {'project': project}
        r = self._client.execute(SPIDER_RESTFUL_CONF['listjobs']['method'],
                                 SPIDER_RESTFUL_CONF['listjobs']['path'],
                                 parameters)
        return json.loads(bytes.decode(r.content))

    def delete_version(self, project, version):
        """
        Delete a project version. If there are no more versions available for a given project,
        that project will be deleted too.
        :param project: (string, required) - the project name
        :param version: (string, required) - the project version
        :return: Json response dic
        """
        parameters = {'project': project, 'version': version}
        r = self._client.execute(SPIDER_RESTFUL_CONF['delversion']['method'],
                                 SPIDER_RESTFUL_CONF['delversion']['path'],
                                 parameters)
        return json.loads(bytes.decode(r.content))

    def delete_project(self, project):
        """
        Delete a project and all its uploaded versions.
        :param project:  (string, required) - the project name
        :return: Json response dic
        """
        parameters = {'project': project}
        r = self._client.execute(SPIDER_RESTFUL_CONF['delproject']['method'],
                                 SPIDER_RESTFUL_CONF['delproject']['path'],
                                 parameters)
        return json.loads(bytes.decode(r.content))
